# PreparedStatementWriter.java

    package org.codefilarete.stalactite.sql.statement.binder;

    import java.lang.invoke.MethodHandleInfo;
    import java.lang.invoke.SerializedLambda;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.function.Function;

    import org.codefilarete.reflection.MethodReferenceCapturer.MethodDefinition;
    import org.codefilarete.tool.function.SerializableThrowingFunction;
    import org.codefilarete.tool.function.SerializableThrowingTriConsumer;
    import org.codefilarete.tool.function.ThrowingTriConsumer;

    import static org.codefilarete.reflection.MethodReferenceCapturer.giveArgumentTypes;
    import static org.codefilarete.reflection.MethodReferences.buildSerializedLambda;

    /**
     * An interface that allows to use {@link PreparedStatement#setXXX(...)} methods to be used as method reference.
     * See {@link DefaultPreparedStatementWriters} for some available ones.
     * See {@link ResultSetReader} for its equivalence for reading {@link java.sql.ResultSet}, or
     * {@link ParameterBinder} to accomplish both.
     *
     * @author Guillaume Mary
     * @see ResultSetReader
     * @see DefaultPreparedStatementWriters
     */
    public interface PreparedStatementWriter<I> extends JdbcBinder<I> {

    	/**
    	 * Creates a {@link PreparedStatementWriter} from a method reference to a {@link PreparedStatement} setter method.
    	 * The type handled by the writer is inferred from the method reference.
    	 *
    	 * @param preparedStatementSetter a method reference to a {@link PreparedStatement} setXXX method
    	 * @param <O> the type of the value to be set on the {@link PreparedStatement}
    	 * @return a {@link PreparedStatementWriter} that invokes the specified {@link PreparedStatement} method
    	 */
    	static <O> PreparedStatementWriter<O> ofMethodReference(SerializableThrowingTriConsumer<PreparedStatement, Integer, O, SQLException> preparedStatementSetter) {
    		// because the targeted method is expected to be one of the setXXX(index, value), we collect the argument at index 1, not zero.
    		Class<O> argumentType = giveArgumentTypes(buildSerializedLambda(preparedStatementSetter)).getArgumentTypes()[1];
    		return new LambdaPreparedStatementWriter<>(preparedStatementSetter, argumentType);
    	}

    	/**
    	 * Applies <code>value</code> at position <code>valueIndex</code> on <code>statement</code>.
    	 *
    	 * @param preparedStatement PreparedStatement to be used
    	 * @param valueIndex parameter index to be set, value for first parameter of methods <code>Statement.setXXX(..)</code>
    	 * @param value value to be passed as second argument of methods <code>Statement.setXXX(..)</code>
    	 * @throws SQLException the exception thrown be the underlying access to the {@link PreparedStatement}
    	 */
    	void set(PreparedStatement preparedStatement, int valueIndex, I value) throws SQLException;

    	/**
    	 * Builds a new {@link PreparedStatementWriter} from this one by applying a converter on the input object
    	 *
    	 * @param converter the {@link Function} that turns input value to the colum type
    	 * @param <O> type of input accepted by resulting {@link PreparedStatementWriter}
    	 * @return a new {@link PreparedStatementWriter} based on this one plus a converting {@link Function}
    	 * @see ResultSetReader#thenApply(SerializableThrowingFunction)
    	 */
    	default <O> PreparedStatementWriter<O> preApply(SerializableThrowingFunction<O, I, ? extends Throwable> converter) {
    		SerializedLambda methodReference = buildSerializedLambda(converter);
    		MethodDefinition methodDefinition = giveArgumentTypes(methodReference);
    		Class<O> argumentType;
    		if (methodReference.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic) {
    			// static method reference : input of function is the type of our new PreparedStatementWriter
    			argumentType = methodDefinition.getArgumentTypes()[0];
    		} else {
    			if (methodDefinition.getArgumentTypes().length == 0) {
    				// method reference is "regular one" : one of a class
    				argumentType = methodDefinition.getDeclaringClass();
    			} else {
    				// case of a captured argument : either an instance method reference, or a real lambda expression
    				argumentType = methodDefinition.getArgumentTypes()[0];
    			}
    		}

    		return new LambdaPreparedStatementWriter<O>((ps, valueIndex, value) -> {
    			try {
    				this.set(ps, valueIndex, converter.apply(value));
    			} catch (Throwable e) {
    				throw new RuntimeException(e);
    			}
    		}, argumentType) {
    			@Override
    			public <U> Class<U> getColumnType() {
    				return PreparedStatementWriter.this.getColumnType();
    			}
    		};
    	}

    	/**
    	 * Class that helps to wrap a {@link PreparedStatement} method as a {@link PreparedStatementWriter}
    	 * @param <I> type written by the writer
    	 * @author Guillaume Mary
    	 */
    	class LambdaPreparedStatementWriter<I> implements PreparedStatementWriter<I> {

    		private final ThrowingTriConsumer<PreparedStatement, Integer, I, SQLException> delegate;

    		private final Class<I> type;

    		public LambdaPreparedStatementWriter(ThrowingTriConsumer<PreparedStatement, Integer, I, SQLException> delegate, Class<I> type) {
    			this.delegate = delegate;
    			this.type = type;
    		}

    		@Override
    		public Class<I> getType() {
    			return type;
    		}

    		@Override
    		public void set(PreparedStatement preparedStatement, int valueIndex, I value) throws SQLException {
    			delegate.accept(preparedStatement, valueIndex, value);
    		}
    	}
    }